Add type annotations for primary key types by model [FC-0117]#534
Add type annotations for primary key types by model [FC-0117]#534bradenmacdonald wants to merge 1 commit intomainfrom
Conversation
|
Thanks for the pull request, @bradenmacdonald! This repository is currently maintained by Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review. 🔘 Get product approvalIf you haven't already, check this list to see if your contribution needs to go through the product review process.
🔘 Provide contextTo help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:
🔘 Get a green buildIf one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green. DetailsWhere can I find more information?If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources: When can I expect my changes to be merged?Our goal is to get community contributions seen and reviewed as efficiently as possible. However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:
💡 As a result it may take up to several weeks or months to complete a review and merge your PR. |
|
@ormsbee @kdmccormick Can I get your thoughts on this? It also has implications for OEP-68. |
| # Note: Django-Stubs forces mypy to identify the `.pk` attribute of this model as having 'Any' type (due to our | ||
| # use of a OneToOneField primary key), and this is impossible for us to override, so we prefer to use | ||
| # `.id` which we can control fully. | ||
| # Since Django uses '.pk' internally, we have to make sure it still works, however. So the best we can do is | ||
| # override this with a deprecated marker, so it shows a warning in developer's IDEs like VS Code. |
There was a problem hiding this comment.
Ouch. Very timely to know this given the OEP in flight on identifiers, where we recommend using .pk.
There was a problem hiding this comment.
Yes, as you can read above in the edited description, I actually changed my approach here, as it turns out it's much easier to override the type of .id than .pk in general. So I'm wondering if we should just recommend .id / _id more.
There was a problem hiding this comment.
oof, good find. I've opened a thread here: openedx/openedx-proposals#773 (comment)
There was a problem hiding this comment.
If we standardize on .id, then I presume we can drop this deprecation warning? I think most developers naturally use .id anyway.
There was a problem hiding this comment.
I found that .pk was being used in quite a few places in this codebase, and the deprecation warnings made it easy for me to track them down and replace them with .id.
|
If we want to also introduce typed keys for Unit/Section/Subsection subclasses, that's easy to do: #535 . Would welcome thoughts on this as I'm a bit on the fence. |
|
OK, good news! I found a nicer way to do this. It still requires a custom field type per model, but we can put the definitions entirely within the model class itself, and there is now one global e.g. within the Note: This could be made even more concise, and copy-pastable without changes: ID = NewType("ID", int)
class IDField(TypedBigAutoField[ID]): ...
id = IDField(primary_key=True)
Edit: actually this works great, so I think I prefer the second version now. If you have several types called |
9d0d7c4 to
cd54e54
Compare
|
@bradenmacdonald woohoo! The TypedBigAutoField is a huge improvement. I was fiddling with my own version of TypedBigAutoField last night and decided that it must be impossible :P Nice work on the
I'm a little iffy on this--this is a pretty painful error message to read. I think I'd prefer more boilerplate in exchange for more-legible messages: |
|
@kdmccormick OK, good call. I changed that back. It now looks like: |
|
@kdmccormick Here is the corresponding platform PR which shows the required changes (pretty minimal) plus demonstrates using the new field to make |
| MyModelID = NewType("MyModelID", int) | ||
| type ID = MyModelID | ||
|
|
||
| class IDField(TypedBigAutoField[ID]): | ||
| pass | ||
|
|
||
| id = IDField(primary_key=True) |
There was a problem hiding this comment.
Not for this PR, but as a possible followup: at the expense of some more boilerplate, what do you think about extending this same system to model relationships?
| MyModelID = NewType("MyModelID", int) | |
| type ID = MyModelID | |
| class IDField(TypedBigAutoField[ID]): | |
| pass | |
| id = IDField(primary_key=True) | |
| # Boilerplate for strongly-typed model relationships. | |
| # Replace "MyModel" with your model's name. | |
| MyModelID = NewType("MyModelID", int) | |
| type ID = MyModelID | |
| class IDField(TypedBigAutoField[ID]): | |
| pass | |
| class ForeignKey(TypedForeignKey[ID]): | |
| def __init__(self, *args, **kwargs): | |
| super().__init__("MyModel", *args, **kwargs) | |
| class OneToOneField(TypedOneToOneField[ID]) | |
| def __init__(self, *args, **kwargs): | |
| super().__init__("MyModel", *args, **kwargs) | |
| id = IDField(primary_key=True) | |
| # To make references to other strongly-typed models: | |
| my_related_object = MyOtherModel.ForeignKey(on_delete=..., ...) | |
| my_one_to_one_object = MyAdditionalModel.OneToOneField(on_delete=..., ...) | |
| # etc. |
maybe there's even some way to shove super().__init__("MyModel", *args, **kwargs) into TypedForeignKey/TypedOneToOneField, so the body of all three field classes can be pass.
There was a problem hiding this comment.
@kdmccormick It's not necessary! The django-stubs plugin should already be providing the correct types for all the foreign key _id fields 😎
Please let me know if you observe any exceptions or if I'm wrong.
| migrations.AlterField( | ||
| model_name="publishableentity", | ||
| name="id", | ||
| field=models.BigAutoField(primary_key=True, serialize=False), |
There was a problem hiding this comment.
Just noting here that changing to these fully-typed primary keys does not result in any migration if the model previous had an explicit id field. But if it had only the implicit, auto-created field, then adding an explicit one results in this kind of migration. By default such migrations will re-create the table in order to re-create the ID column, even though nothing has changed, so it's important to catch that and tweak the migration like this to not actually make any DB changes.
feat: added typed primary keys for Container feat: added typed primary keys for Component feat: added typed primary keys for PublishableEntity feat: added typed primary keys for LearningPackage feat: added typed primary keys for CatalogCourse and CourseRun feat: added typed primary keys for Units/Subsections/Sections refactor: change typed ID fields to use 'id' instead of 'pk' feat: add a default 'id' accessor to PublishableEntityMixin
bd68671 to
96343db
Compare
This PR has some experimental implementations of #436 as well as moving closer toward the consistent terms preferred in OEP-68.
Second Approach: fully-typed
.id, avoid.pk(Revised from my initial attempt)
PublishableEntity,LearningPackage,Container, andComponentall get a fully-typed.idfield.PublishableEntity.idis of typePublishableEntity.PKwhich is a sub-type ofint.Container.idis of typeContainer.PKwhich is a sub-type ofPublishableEntity.PK.Component.idis of typeComponent.PKwhich is a sub-type ofPublishableEntity.PK.LearningPackage.idis of typeLearningPackage.PKwhich is a sub-type ofint.CatalogCourseandCourseRunget typed..pkon models that inheritPublishableEntityMixin(or any other models that use aOneToOneFieldas theirprimary_key, at least without modifying django-stubs or implementing a mypy plugin. It will always have typeAny, circumventing all type definitions. However, although we cannot override its type, we can mark it asdeprecated. As a result, and for consistency, accessing.pkon any of these will raise a deprecation warning in pytest/console and show a warning in your IDE:CatalogCourse.pkandCourseRun.pk, because (a) they are typed correctly, and (b) mypy crashes when I do 😬.Prior Approach: fully-typed
.pk, avoid.idExpand for pros/cons/details of this earlier approach
In this earlier approach:PublishableEntity's primary key is the.pkfield, and it has typePublishableEntity.PKwhich is a sub-type ofint.some_entity.idwill show a deprecation warning, telling you to usepkinstead. And since this is an actual field calledpk, any DB lookups need to be changed from e.g.entity__id=...toentity__pk=...idand make an alias calledentity_pk, then use deprecation warnings to encourage the use ofentity_pkoveridandpk; this would be more consistent with what I did for containers and components, but it's slightly annoying to typeentity_pkinstead ofpkeverywhere.idand makingpka fully-typed alias is unfortunately not possible, as django-stubs forcibly overrides the type ofpkunless you have a concrete field namedpkas I did here.ContainerandComponentgetContainer.PKandComponent.PKtypes which are sub-types ofPublishableEntity.PKContainerandComponentcannot have correctly-typed.pkaccessors due to django-stubs limitations, so I created a.container_pkand.component_pkaccessor for them:ContainerandComponenthave aOneToOneField(PublishsableEntity, primary_key=True), django-stubs will always override theirpkattribute to have typeAny, and it's impossible to change this without modifying django-stubs or writing our own mypy plugin. But, we can use the@deprecateddecorator to annotate the attribute with a deprecation warning which does show up in the IDE and in the pytest logs wherever.pkis used..pk_?)General Notes
One downside of using deprecation warnings as I have here is that Django itself uses
.pk, so in the case ofContainerandComponent(but notPublishableEntity) where we've marked.pkas deprecated, even if our code doesn't use.pkitself, you'll see warnings like this when running pytest:django/db/models/fields/related.py:812: DeprecationWarning: Use component_pk instead. It is possible to suppress these deprecation warnings, and I have done so (seetox.ini) but it's an extra step required to use this library well and I'm not a big fan of it.